package cn.lingyangwl.agile.misc.statistic.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.math.Money;
import cn.lingyangwl.agile.misc.statistic.annotation.*;
import cn.lingyangwl.agile.misc.statistic.constant.*;
import cn.lingyangwl.agile.misc.statistic.model.*;
import cn.lingyangwl.agile.misc.statistic.model.entity.*;
import cn.lingyangwl.agile.model.enums.*;
import cn.lingyangwl.agile.model.module.tenant.*;
import cn.lingyangwl.agile.system.*;
import cn.lingyangwl.agile.system.model.entity.*;
import cn.lingyangwl.agile.tenant.core.utils.*;
import cn.lingyangwl.framework.tool.core.*;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * @author shenguangyang
 */
@Slf4j
@Service
public class StatAnalyzeServiceImpl implements StatAnalyzeService {
    private static final Map<Class<?>, List<BaseStatGroupField>> GROUP_FIELD_CACHE = new ConcurrentHashMap<>();

    @Resource
    private SystemApi systemApi;

    @Resource
    private StatCacheService cacheService;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private StatHourRecordService statHourRecordService;
    @Resource
    private StatDayRecordService statDayRecordService;

    @FunctionalInterface
    public interface SaveConsumer<T, U, D> {
        void accept(T t, U u, D d);
    }


    @Override
    public void syncStatistic() {
        Integer pageSize = 500;
        Integer pageNum = 1;
        AtomicInteger count = new AtomicInteger(0);
        String lockKey = "statistic:syncLock";
        if (Boolean.TRUE.equals(redisTemplate.hasKey(lockKey))) {
            log.info("已有线程正在同步数据, 本次同步无效");
            return;
        }
        try {
            redisTemplate.opsForValue().set(lockKey, "1", 1, TimeUnit.DAYS);
            LocalDateTime nowDate = LocalDateTime.now();
            while (true) {
                List<SysTenant> list = systemApi.pageTenant(pageNum, pageSize);
                if (CollUtil.isEmpty(list)) {
                    log.info("统计结束, 共统计 {} 个租户数据", count.get());
                    return;
                }

                Set<String> tenantIds = list.stream().map(SysTenant::getId).collect(Collectors.toSet());
                tenantIds.addAll(list.stream().map(SysTenant::getMainTenantId).collect(Collectors.toSet()));
                log.info("开始同步统计数据, 当前租户 pageNum: {}, pageSize: {}", pageNum, pageSize);
                tenantIds.forEach(tenantId -> TenantUtils.execute(tenantId, () -> {
                    int delta = 0;
                    delta = saveData(delta, "统计天维度数据", nowDate, StatDayRecord::new, statDayRecordService::saveData);
                    delta = saveData(delta, "统计小时维度数据", nowDate, StatHourRecord::new, statHourRecordService::saveData);
                    count.addAndGet(delta);
                }));
                pageNum++;
            }
        } finally {
            redisTemplate.delete(lockKey);
        }
    }

    public <T extends StatBasePO> int saveData(int delta, String title, LocalDateTime nowDate, Supplier<T> newObj,
                                               SaveConsumer<String, LocalDateTime, List<T>> doSave) {
        String tenantId = TenantContext.get().getTenantId();
        try {
            T emptyEntity = newObj.get();
            StatisticEntityConfig entityAnnot = emptyEntity.getClass().getAnnotation(StatisticEntityConfig.class);
            Class<? extends Enum<?>> enumClass = Optional.ofNullable(entityAnnot).map(StatisticEntityConfig::value)
                .map(GroupField::value).orElse(null);

            String groupFieldName = Optional.ofNullable(entityAnnot).map(StatisticEntityConfig::value)
                .map(GroupField::name).orElse(null);

            // 获取分组字段
            List<String> groupFieldValues = Objects.nonNull(enumClass) && StringUtils.isNotEmpty(groupFieldName)
                ? listGroupFieldValue(enumClass) : Collections.singletonList("");

            // 根据分组字段查找缓存中的数据
            Map<String, List<T>> entityMap = groupFieldValues.stream().map(groupFieldValue -> {
                T entity = newObj.get();
                entity.setStatDate(nowDate.toLocalDate());

                if (StringUtils.isNotEmpty(groupFieldName)) {
                    Field field = ReflectUtils.getField(entity, groupFieldName);
                    if (Objects.nonNull(field)) {
                        try {
                            field.set(entity, groupFieldValue);
                        } catch (Exception e) {
                            log.error("error: ", e);
                        }
                    }
                }
                boolean hasData = fillFieldFromCache(nowDate, tenantId, groupFieldValue, entity);
                return hasData ? entity : null;
            }).filter(Objects::nonNull).collect(Collectors.groupingBy(e -> { // 根据分组字段进行分组
                if (StringUtils.isEmpty(groupFieldName)) {
                    return "";
                }
                Field field = ReflectUtils.getField(e, groupFieldName);
                if (Objects.isNull(field)) {
                    return "";
                }
                try {
                    return field.get(e).toString();
                } catch (Exception exception) {
                    log.error("error: ", exception);
                }
                return "";
            }));

            // 完成保存
            if (CollUtil.isNotEmpty(entityMap)) {
                delta = 1;
                entityMap.forEach((k, v) -> {
                    doSave.accept(k, nowDate, v);
                    log.info("[{}] tenantId: {}, 分组字段: {}, 统计结果: {}", title, tenantId, k, JSON.toJSONString(v));
                });

            }
        } catch (Exception e) {
            log.error("[{}] 失败, error: ", title, e);
        }
        return delta;
    }

    /**
     * 从缓存中获取字段数据
     * @return true 有统计数据, false 无统计数据
     */
    public <T extends StatBasePO> boolean fillFieldFromCache(LocalDateTime dateTime, String tenantId, String groupFieldValue, T entity) {
        String entityName = entity.getClass().getSimpleName();
        List<Field> fields = ReflectUtils.getFields(entity);
        AtomicBoolean ret = new AtomicBoolean(false);

        fields.forEach(field -> {
            StatisticField annot = field.getAnnotation(StatisticField.class);
            if (Objects.isNull(annot)) {
                return;
            }

            Dimension dim = annot.dim();
            if (dim == Dimension.DAY) {
                String dateStr = LocalDateTimeUtil.format(dateTime, "yyyy-MM-dd");
                Long count = cacheService.getIncr(tenantId, entityName, dateStr, groupFieldValue, field.getName());
                if (count == 0) {
                    return;
                }
                setEntityValue(entity, field, count);
            } else if (dim == Dimension.HOUR) {
                String[] dateStr = LocalDateTimeUtil.format(dateTime, "yyyy-MM-dd HH").split(" ");
                Map<String, Long> countMap = cacheService.getHashIncr(tenantId, entityName, dateStr[0], dateStr[1], groupFieldValue, field.getName());
                Long count = countMap.values().stream().reduce(Long::sum).orElse(0L);
                if (count == 0) {
                    return;
                }
                setEntityValue(entity, field, count);
            }
            ret.set(true);
        });
        return ret.get();
    }


    /**
     * 获取分组的字段值
     */
    public List<String> listGroupFieldValue(Class<? extends Enum<?>> enumClass) {
        List<BaseStatGroupField> values = GROUP_FIELD_CACHE.get(enumClass);
        if (CollUtil.isEmpty(values)) {
            if (enumClass.isEnum()) {
                Method valuesMethod;
                try {
                    valuesMethod = enumClass.getMethod("values");
                    Enum<?>[] enumValues = (Enum<?>[]) valuesMethod.invoke(null);

                    values = Arrays.stream(enumValues).filter(e -> e instanceof BaseStatGroupField)
                        .map(e -> (BaseStatGroupField) e).collect(Collectors.toList());
                    GROUP_FIELD_CACHE.put(enumClass, values);
                } catch (Exception e) {
                    log.error("error: ", e);
                }
            }
        }
        return values.stream().map(BaseStatGroupField::getValue)
            .distinct().sorted().collect(Collectors.toList());
    }

    @Override
    public void doStatistic(StatisticBaseReq req) {
        LocalDateTime dateTime = Optional.ofNullable(req.getDateTime()).orElse(LocalDateTime.now());
        Class<? extends StatBasePO> entityClass = req.getEntityClass();
        String entityName = entityClass.getSimpleName();
        StatisticEntityConfig entityAnnot = entityClass.getAnnotation(StatisticEntityConfig.class);
        Optional<String> groupFieldName = Optional.ofNullable(entityAnnot).map(StatisticEntityConfig::value)
            .map(GroupField::name);
        // 获取分组的字段值
        String groupFieldValue = groupFieldName.map(fieldName -> {
                try {
                    Field objectField = ReflectUtils.getField(req, fieldName);
                    if (Objects.isNull(objectField)) {
                        log.warn("分组字段 [{}] 不存在于 [{}]", fieldName, req.getClass().getSimpleName());
                        return null;
                    }
                    return objectField.get(req);
                } catch (Exception e) {
                    log.error("error: ", e);
                }
                return null;
            }).map(String::valueOf).orElse("");

        List<Field> reqFields = ReflectUtils.getFields(req);
        reqFields.forEach(reqField -> {
            try {
                Object value = reqField.get(req);
                if (Objects.isNull(value)) {
                    return;
                }

                String reqFieldName = reqField.getName();

                // step 校验目标字段
                Field entityField = ReflectUtils.getField(entityClass, reqFieldName);
                // 排除分组字段
                if (Objects.isNull(entityField) || entityField.getName().equals(groupFieldName.orElse(""))) {
                    return;
                }

                // step 校验类型
                if (!reqField.getType().equals(entityField.getType())) {
                    log.warn("{}.{} 和 {}.{} 字段类型不匹配, 应是 {} 类型",
                        entityName, reqFieldName, req.getClass().getSimpleName(),
                        reqField.getName(), entityField.getType().getSimpleName());
                    return;
                }

                // step 获取字段值并存入到缓存中
                StatisticField entityFieldAnnot = entityField.getAnnotation(StatisticField.class);
                if (Objects.isNull(entityFieldAnnot)) {
                    log.warn("{}.{} 上不存在注解 {}", entityName, entityField.getName(), StatisticField.class.getSimpleName());
                    return;
                }

                Long cacheValue = convertValue(value);
                if (Objects.isNull(cacheValue)) {
                    return;
                }

                log.info("统计租户 [{}] 数据, {}.{} = {}, groupField [{}]",
                    TenantContext.get().getTenantId(), entityName, entityField.getName(), value, groupFieldValue);

                Dimension dim = entityFieldAnnot.dim();
                String mainTenantId = TenantContext.get().getMainTenantId();
                String tenantId = TenantContext.get().getTenantId();
                if (dim == Dimension.DAY) {
                    String dateStr = LocalDateTimeUtil.format(dateTime, "yyyy-MM-dd");
                    cacheService.incr(tenantId, entityName, dateStr, cacheValue,  groupFieldValue, entityField.getName());
                    if (!tenantId.equals(mainTenantId)) {
                        cacheService.incr(mainTenantId, entityName, dateStr, cacheValue,  groupFieldValue, entityField.getName());
                    }
                } else if (dim == Dimension.HOUR) {
                    String[] dateStr = LocalDateTimeUtil.format(dateTime, "yyyy-MM-dd HH").split(" ");
                    cacheService.hashIncr(tenantId, entityName, dateStr[0], dateStr[1], cacheValue,  groupFieldValue, entityField.getName());
                    if (!tenantId.equals(mainTenantId)) {
                        cacheService.hashIncr(mainTenantId, entityName, dateStr[0], dateStr[1], cacheValue,  groupFieldValue, entityField.getName());
                    }
                }
            } catch (Exception e) {
                log.error("error", e);
            }
        });
    }

    public Long convertValue(Object value) {
        if (Objects.isNull(value)) {
            return null;
        }
        if (value instanceof BigDecimal) {
            return new Money((BigDecimal) value).getCent();
        } else if (value instanceof Integer) {
            return ((Integer) value).longValue();
        } else if (value instanceof Long) {
            return (Long) value;
        } else {
            log.warn("尚不支持 [{}] 字段类型的转换", value.getClass().getSimpleName());
        }
        return null;
    }

    public void setEntityValue(Object entity, Field field, Long value) {
        try {
            if (field.getType().equals(BigDecimal.class)) {
                BigDecimal amount = new Money(value).getAmount();
                field.set(entity, amount);
            } else if (field.getType().equals(Integer.class)) {
                field.set(entity, value.intValue());
            } else if (field.getType().equals(Long.class)) {
                field.set(entity, value);
            } else {
                log.warn("尚不支持 [{}] 实体字段类型 [{}] 赋值", value.getClass().getSimpleName(), field.getType().getSimpleName());
            }
        } catch (Exception e) {
            log.error("error: ", e);
        }
    }
}
